<?php
/**
 *  Item can be in 3 states:
 *      1. blank item: has no values yet
 *      2. complete item: all values are initialized to Item_Value objects
 *      3. temporarily incomplete item: not all of the values are set
 *
 */
abstract class Yz_Item_Abstract
    extends Yz_Object
    implements Yz_Item_Interface
{

    /** To distinguish from the public id property, name this one _id
     *  @var int
     */
    protected $_id;

    /**
     *  @var dict { name: Yz_Item_Value_* }
     */
    #protected $__dict__ = array();

    /** the model the item belongs to
     *  @var Yz_Model_Interface
     */
    protected $model;

    /** the item's fields
     *  @var array
     */
    protected $fields;

    /** the item of parent model
     *  @var Yz_Item_Interface
     */
    protected $parentItem;

    /** the item of parent model
     *  @var string
     */
    protected $parentName;



    /************************************
     *          Public methods          *
     ************************************/

    /**
     *  @param Yz_Model_Interface $model
     *  @return ignored
     */
    public function __construct(Yz_Model_Interface $model)
    {
        $this->model = $model;
        $this->fields = $model->toArray();
        $parent = $model->getParentModel();
        if ($parent)
            $this->parentName = $parent->getName();
    }

    /** On cloning, force removal of the item's id and cloning of parent item if any
     *      TODO also remove values for UNIQUE fields
     *  @return ignored
     */
    public function __clone()
    {
        $this->_id = null;
        if ($this->__dict__) // in case item empty, do not set ID
            $this->__dict__['id'] = null;
        else if ($this->parentItem) { // parent items exist only for non-empty items
            // also if item has a parent, clone it, too, and replace reference in __dict__
            $this->parentItem = clone $this->parentItem;
            $this->__dict__[$this->parentName] = $this->parentItem;
        }
    }


    /** Return the item's string representation
     *  @return string
     */
    public function __toString()
    {
        $name = $this->_class_();
        $n = explode('\\', $name, 3);
        if (isset($n[2]))
            $name = strtr($n[2], '\\_', '..');
        $name .= '(' . $this->_id . ')';
        return $name;
    }

    /**
     *  NOTE: ### what about parent-child item and accessor methods
     */
    public function toArray()
    {
        $my =& $this->__dict__;
        if (! $my)
            $this->init();
        return $my;
    }


    /** Item is empty if it has no values
     *  @return bool
     */
    public function isEmpty()
    {
        return !$this->__dict__;
    }

    /** describe an item, i.e. get its model
     *  @return Yz_Model_Interface $model
     */
    final public function describe()
    {
        return $this->model;
    }

    /** Assign values from an action
     *
     */
    public function assignValues( Yz_Action_Interface $action )
    {
        debug();
        assert('!$this->_id');
        return $this->_assignValues($action, '');
    }

    /** Assign default (or preset) values for the listed fields, or for all fields if none listed
     *  @param array|null fields {name: Yz_Model_Field_*}
     *  @return this
     */
    public function newValues($fields=null)
    {
        debug();
        if (null !== $fields) {
            $fields = Yz_Hash::valueOf($fields);
            if (null === $fields)
                throw TypeError::_new_();
            if (! $this->__dict__)
                $this->init();
        }
        return $this->_newValues($fields);
    }

    /**
     *
     */
    public function getId()
    {
        return $this->_id;
    }

    /**
     *  @throw ImmutableAttributeError if item's id is already > 0
     */
    public function setId($id)
    {
        if ($this->_id or $this->__dict__)
            throw ImmutableAttributeError::_new_(array('attr' => 'id'));
        assert('is_numeric($id) and $id > 0');
        $this->_id = (int)$id;
        return $this->fetch();

        //$id = $this->fields['id']->testValue($id);
    }


    /**
     *  get item's title from its 'title' or 'name' value, or from its tostring method
     *  @return string
     *  @todo must modify the method to allow custom getters to be used
     */
    public function getTitle()
    {
        $v =& $this->__dict__;
        if (! $v)
            $this->init();
        if (isset($v['title']))
            $title = $v['title'];
        else if (isset($v['name']))
            $title = $v['name'];
        else
            $title = $this->__toString();
        return $title;
    }

    /**
     *  save item to the model
     *      Use model's update/add depending on whether the ID is empty
     *  @return this
     *  @throw StateError if item's id is not set
     */
    public function neighbors()
    {
        if (! $this->_id)
            throw StateError::_new_();
        $result = $this->model
            ->selectAction('SelectNeighbors')
            ->setItem($this)
            ->perform()
        ;
        $prevItem = $result->fetch()
            and $prevItem = $this->model->item()->_assignValues($prevItem);
        $nextItem = $result->fetchNext()
            and $nextItem = $this->model->item()->_assignValues($nextItem);

        $neighbors = array($prevItem, $nextItem);
        return new Yz_List($neighbors, $this);
    }

    /**
     *  save item to the model
     *      Use model's update/add depending on whether the ID is empty
     *  @return this
     */
    public function save()
    {
        if (! $this->__dict__)
            throw StateError::_new_();
        if ($this->_id)
            // update handles parents/children correctly using INNER JOIN
            $this->model->updateItem($this);
        else {
            $id =& $this->__dict__['id'];
            $id = null;
            if ($this->parentItem) {
                $this->_id = $id = $this->parentItem->save()->_id;
                debug("CONTINUE INSERT, id='$id'");
            }
            $id = $this->model->insertItem($this);
            assert('is_int($id) and $id > 0');
            $this->_id = $id;
            $this->model->getItems()->add($this);
        }
        return $this;
    }

    /**
     *  delete item from the model
     *  @return this
     *  @throw StateError if item's id is not set
     *  @todo TODO forward the delete call down the item inheritance chain
     */
    public function delete()
    {
        if (! $this->_id)
            throw StateError::_new_();
        $result = $this->model
            ->selectAction('DeleteItem')
            ->setItem($this)
            ->perform()
        ;
        // also removes parent items from their lists
        $this->model->getItems()->remove($this);
        // this.dict is shared by all parent items
        $this->__dict__ = array();
        $this->_id = null;
        $item = $this;
        while ($item = $item->parentItem)
            $item->_id = null;
        return null;
    }


    /*******************************
     *      Protected methods      *
     *******************************/


    /** validate the index, adjust it if required/possible
     *  NOTE: called AFTER accessor method check
     *   @param mixed index
     *   @return mixed valid index
     *   @throw TypeError if index type not valid
     *   @throw AttributeError if index value not valid
     */
    protected function adjustIndex( $index )
    {
        $index = parent::adjustIndex( $index );
        if (! isset($this->fields[$index]))
            throw AttributeError::_new_(array('attr' => $index));
        if (! $this->__dict__)
            $this->init();
        return $index;
    }

    /** get the value, after adjustIndex showed that such field exists
     *  @return this
     *  @throw AttributeError
     *  @throw StateError
     */
    protected function _get_( $var )
    {
        return $this->__dict__[$var];
    }

    /** test the value being set, using the field's testValue()
     *  @return this
     *  @throw AttributeError
     *  TODO forward new value to the parent if it exists
     */
    protected function _set_( $var, $value )
    {
        $field = $this->fields[$var];
        if ($field->getReadonly())
            throw ImmutableAttributeError::_new_(array('attr' => $var));
        $this->__dict__[$var] = $value = $field->testValue($value);
        #if ($this->parentItem and isset($this->parentItem->fields[$var]))
            // assert('$this->parentItem is initialized');
        #    $this->parentItem->__dict__[$var] = $value;
        return $this;
    }

    /** unsetting variables is equal to setting a default value
     *  @throw AttributeError
     *  TODO forward new value to the parent if it exists
     */
    protected function _del_( $var )
    {
        $this->__dict__[$var] = $this->fields[$var]->newValue();
        return $this;
    }


    /** Complete the item's values if they are not complete :)
     *  @return this
     */
    protected function init()
    {
        debug();
        if ($this->_id) {
            // uninitialized item with id
            // load item from storage
            $this->fetch();
        } else {
            // blank item
            // fill the values with the defaults (or actually preset values)
            $this->_newValues();
        }
        return $this;
    }


    /** Fetch item in case it is not present on the list
     *  @return this
     *  @throw NotFoundError
     */
    protected function fetch()
    {
        $result = $this->model
            ->selectAction('SelectById')
            ->setId($this->_id)
            ->fetch()
        ;
        return $this->_assignValues($result, "");
    }


    /** Assign default values
     *  @param array fields
     *  @return
     */
    protected function _newValues($newFields=null)
    {
        debug();
        // Partial fields assignment can take place only for initialized `complete` items
        assert('null === $newFields or !! $this->__dict__');

        // ### forward call to parent item, create it if necessary
        if ($this->parentName) {
            if (! $this->parentItem)
                $this->parentItem = $this->model->getParentModel()->item();
                #$this->parentItem = $this->model->getParentModel()->item()->_newValues();
            $this->__dict__ =& $this->parentItem->__dict__; // items share the same properties
            $this->__dict__[$this->parentName] = $this->parentItem;
        }

        // since this.dict references the parent item's dict,
        // the values will be forwarded to the parent automatically
        $my =& $this->__dict__;
        foreach ($this->fields as $var => $field) {
            debug("Assign value: {$var}={$field}");

            $assign = true;
            if ($field instanceof Yz_Model_Field_ParentModel)
                $assign = false;
            else if ($newFields) { // if `new` fields are given,
                if (isset($newFields[$var])) { // and the $var field is present,
                    if ($newFields[$var] !== $field) // it must be the same field object as the item's respective field
                        throw TypeError::_new_(array('argument' => $var));
                } else { // if $var is missing,
                    $assign = false; // do not assign a value (keep old)
                }
            }
            if ($assign)
                $my[$var] = $field->newValue();
        }
        return $this;
    }

    /**
     *  @param action
     *  @param string prefix
     */
    protected function _assignValues( Yz_Action_Interface $action, $prefix="" )
    {
        debug();
        if ($this->parentName) {
            $this->parentItem = $this->model->getParentModel()->item();
            $this->parentItem->_assignValues($action, $prefix . $this->parentName .'_');
            $this->__dict__ =& $this->parentItem->__dict__; // items share the same properties
            $this->__dict__[$this->parentName] = $this->parentItem;
        }

        $my =& $this->__dict__;
        $items = $this->model->getItems();
        foreach ($this->fields as $var => $field) {
            // value already set by the parent item
            if (array_key_exists($var, $my))
                continue;

            // the parent item already initialized and set, so it will not appear here
            // if ($field instanceof Yz_Model_Field_ParentModel)

            $value = $action[$prefix . $var]; // throw AttributeError if not exists
            if ($field instanceof Yz_Model_Field_SelfReference) {
                // reference to self

                if (null !== $value) {
                    $id = (int)$value;
                    // try to get value if it exists
                    $value = $items->offsetGetIfExists($id);
                    if (! $value) {
                        // if no item exists, create empty (`incomplete`) item with id only
                        $value = new $this($this->model);
                        $value->_id = $id;
                        $items->add($value);
                    }
                }
            } else if ($field instanceof Yz_Model_Field_Reference) {
                debug("Field $var: '$value'");
                // recurse into field
                if (null !== $value) {
                    $id = (int)$value;
                    $refItems = $field->getModel()->getItems();
                    // try to get value if it exists
                    $value = $refItems->offsetGetIfExists($id);
                    if (! $value) {
                        // create new one and assign
                        $value = $field
                            ->getModel()
                            ->item()
                            ->_assignValues($action, $prefix.$var.'_')
                        ;
                    }
                }
            } else {
                $value = $field->assignValue($value);
            }
            $my[$var] = $value;
        }
        // the key 'id' is defined, according to foreach(fields)
        assert('$my["id"] > 0');
        $this->_id = $my["id"];
        debug($this->_id);
        $items->add($this);
        return $this;
    }

}
